今天繼續實作 useMouse,讓他的功能更完整,可以搭配 Day 11 的基本功能實作一起看~
昨天有提到,useMouse 流程的開始是透過 useEventListener
,所以昨天的實作透過 useEventListener
監聽 mousemove、dragover 事件。現在要加入 touch event,一樣從這邊開始
// src/compositions/useMouse.js
const UseMouseBuiltinExtractors = {
page: event => [event.pageX, event.pageY],
client: event => [event.clientX, event.clientY],
screen: event => [event.screenX, event.screenY],
}
export function useMouse(options = {}) {
const {
type = 'page',
touch = true,
initialValue = { x: 0, y: 0 },
window = defaultWindow,
target = window,
} = options
const x = ref(initialValue.x)
const y = ref(initialValue.y)
const extractor = typeof type === 'function'
? type
: UseMouseBuiltinExtractors[type]
const touchHandler = (event) => {
if (event.touches.length > 0) {
const result = extractor(event.touches[0])
if (result) {
[x.value, y.value] = result
}
}
}
const touchHandlerWrapper = event => touchHandler(event)
if (target) {
const listenerOptions = { passive: true }
// 加入 touch event
if (touch && type !== 'movement') {
useEventListener(target, ['touchstart', 'touchmove'], touchHandlerWrapper, listenerOptions)
}
}
}
touch 是 useMouse 的其中一個參數,可以決定要不要監聽 touchstart、touchmove 事件,if (touch && type !== 'movement')
有特別排除 movement,因為 movement 是屬於 mouseEvent 的屬性,並且在非 mousemove 事件的值都會是 0。
movement mdn:https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/movementX
在 touchstart、touchmove 事件觸發後,會執行 event => touchHandler(event)
,接下來看到 touchHandler
這段,跟昨天講到的 mouseHandler
差不多,到這邊 touch event 功能就完成了。
先前實作的 useMouse 回傳值是 x、y,看官網 Demo 可以看到還有一個 source type,剛剛加入 touch event 後,可以透過回傳的 source type 讓上層使用者知道目前是觸發 mouse 或是 touch。
// src/compositions/useMouse.js
export function useMouse(options = {}) {
// ...略
const sourceType = ref(null)
const mouseHandler = (event) => {
// ...略
if (result) {
sourceType.value = 'mouse'
}
}
const touchHandler = (event) => {
// ...略
if (result) {
sourceType.value = 'touch'
}
}
}
這個參數官網文件好像沒有特別提到,如果傳入 true 的話,會在 touchend 事件觸發時,把 x, y 值設定成另一個參數 initialValue
的值,initialValue
沒有傳的話,預設是 { x: 0, y: 0 }
。
// src/compositions/useMouse.js
export function useMouse(options = {}) {
const {
type = 'page',
touch = true,
initialValue = { x: 0, y: 0 },
resetOnTouchEnds = false, // <- 加參數
window = defaultWindow,
target = window,
} = options
// ...略
const reset = () => {
x.value = initialValue.x
y.value = initialValue.y
}
// ...略
if (touch && type !== 'movement') {
useEventListener(target, ['touchstart', 'touchmove'], touchHandlerWrapper, listenerOptions)
if (resetOnTouchEnds) { // <- 加在這邊
useEventListener(target, 'touchend', reset, listenerOptions)
}
}
}
目前還有個問題,就是頁面在滾動的時候,x、y 座標並沒有更新
有個 PR 處理這個 issue:https://github.com/vueuse/vueuse/pull/3244
像 PR 中說明的,這個功能只針對 target 是 window 同時 type 為 page 的情境。
// src/compositions/useMouse.js
// ...略
let _prevMouseEvent = null
const mouseHandler = (event) => {
// ... 略
_prevMouseEvent = event
// ...略
}
const scrollHandler = () => {
if (!_prevMouseEvent || !window)
return
const pos = extractor(_prevMouseEvent)
if (_prevMouseEvent instanceof MouseEvent && pos) {
// 以 document 左上角為原點(0, 0),所以鼠標當前位置要加上滾動的距離
x.value = pos[0] + window.scrollX
y.value = pos[1] + window.scrollY
}
}
// ...略
const scrollHandlerWrapper = event => scrollHandler(event)
if (target) {
const listenerOptions = { passive: true }
useEventListener(target, ['mousemove', 'dragover'], mouseHandlerWrapper, listenerOptions)
if (touch && type !== 'movement') {
useEventListener(target, ['touchstart', 'touchmove'], touchHandlerWrapper, listenerOptions)
}
if (scroll && type === 'page') // <- 加在這
useEventListener(window, 'scroll', scrollHandlerWrapper, listenerOptions)
}
以上實作的 GitHub PR:https://github.com/RhinoLee/30days_vue/pull/2/files#diff-028451d2fc166c9a050ed78fcfebe2712e1457dad45a9f7e6e8891b8858ec048
針對三個 event listener wrapper 進行調整,加入 eventFilter 參數後
// src/compositions/useMouse.js
// ...略
const mouseHandlerWrapper = eventFilter
? event => eventFilter(() => mouseHandler(event), {})
: event => mouseHandler(event)
const touchHandlerWrapper = eventFilter
? event => eventFilter(() => touchHandler(event), {})
: event => touchHandler(event)
const scrollHandlerWrapper = eventFilter
? () => eventFilter(() => scrollHandler(), {})
: () => scrollHandler()
// ...略
看到這個 eventFilter 似乎有一種熟悉的感覺,其實 Day 2 一開始講到的 useThrottleFn
這個 vueuse API,就有實作到他的核心 throttleFilter
,詳細可以從 Day 3 開始看。
也就是說,可以把這個 throttleFilter
當作參數傳給 useMouse,這樣我們事件觸發執行 listener 的時候就可以有 throttle 的效果了!而這些 filter 也可以單獨從 vueuse import 到專案,可以很彈性的做搭配~
參考官方文件:https://vueuse.org/guide/config.html#event-filters
文件是用 throttle 的好兄弟 debounce 當範例。
我的 eventFilter option GitHub PR:https://github.com/RhinoLee/30days_vue/pull/10/files
最後這個 eventFilter 呼應到一開始實作的 throttleFilter
,覺得這個設計很棒,useMouse 這樣的功能應該很常會用到這些 filter 來做效能最佳化,沒想到是這樣巧妙的設計~
useMouse 到這邊告一段落,明天會從 useMouseInElement 這個 API 開始講~